Prozkoumejte výkonné alternativy k enumům v TypeScriptu: const assertions a union types. Naučte se, kdy kterou použít pro robustní a udržovatelný kód.
Mimo Enumů: TypeScript Const Assertions vs. Union Types
Ve světě staticky typovaného JavaScriptu s TypeScriptem byly enumy dlouho oblíbené pro reprezentaci pevné sady pojmenovaných konstant. Nabízejí jasný a čitelný způsob, jak definovat kolekci souvisejících hodnot. Jak ale projekty rostou a vyvíjejí se, vývojáři často hledají flexibilnější a někdy i výkonnější alternativy. Dva silní konkurenti, kteří se často objevují, jsou const assertions a union types. Tento příspěvek se zabývá nuancemi používání těchto alternativ k tradičním enumům, poskytuje praktické příklady a radí vám, kdy si kterou vybrat.
Porozumění tradičním TypeScript Enumům
Než prozkoumáme alternativy, je nezbytné pevně pochopit, jak standardní TypeScript enumy fungují. Enumy vám umožňují definovat sadu pojmenovaných číselných nebo řetězcových konstant. Mohou být číselné (výchozí) nebo řetězcové.
Číselné Enumy
Ve výchozím nastavení jsou členům enumu přiřazeny číselné hodnoty počínaje 0.
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Output: 0
Můžete také explicitně přiřadit číselné hodnoty.
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Output: 200
Řetězcové Enumy
Řetězcové enumy jsou často preferovány pro jejich lepší ladění, protože názvy členů jsou zachovány v kompilovaném JavaScriptu.
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Output: "BLUE"
Režie Enumů
Zatímco enumy jsou pohodlné, přicházejí s mírnou režií. Při kompilaci do JavaScriptu se TypeScript enumy mění na objekty, které mají často reverzní mapování (např. mapování číselné hodnoty zpět na název enumu). To může být užitečné, ale také to přispívá k velikosti balíčku a nemusí to být vždy nutné.
Zvažte tento jednoduchý řetězcový enum:
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
V JavaScriptu se to může stát něčím takovým:
var Status;
(function (Status) {
Status["Pending"] = "PENDING";
Status["Processing"] = "PROCESSING";
Status["Completed"] = "COMPLETED";
})(Status || (Status = {}));
Pro jednoduché sady konstant jen pro čtení se tento generovaný kód může zdát trochu nadměrný.
Alternativa 1: Const Assertions
Const assertions jsou výkonná funkce TypeScriptu, která vám umožňuje říct kompilátoru, aby odvodil co nejspecifičtější typ pro hodnotu. Při použití s poli nebo objekty určenými k reprezentaci pevné sady hodnot mohou sloužit jako odlehčená alternativa k enumům.
Const Assertions s Poli
Můžete vytvořit pole řetězcových literálů a poté použít const assertion, aby byl jeho typ neměnný a jeho prvky byly literální typy.
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Error: Type '"FAILED"' is not assignable to type 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Processing status: ${status}`);
}
processStatus("COMPLETED");
Pojďme si rozebrat, co se zde děje:
as const: Tato assertion říká TypeScriptu, aby s polem zacházel jako s polem jen pro čtení a odvodil nejpřesnější literální typy pro jeho prvky. Takže místo `string[]` se typ stane `readonly ["PENDING", "PROCESSING", "COMPLETED"]`.typeof statusArray[number]: Toto je mapovaný typ. Iteruje přes všechny indexystatusArraya extrahuje jejich literální typy. Indexový podpisnumberv podstatě říká "dej mi typ libovolného prvku v tomto poli." Výsledkem je union type:"PENDING" | "PROCESSING" | "COMPLETED".
Tento přístup poskytuje typovou bezpečnost podobnou řetězcovým enumům, ale generuje minimální JavaScript. Samotné statusArray zůstává polem řetězců v JavaScriptu.
Const Assertions s Objekty
Const assertions jsou ještě výkonnější, když se aplikují na objekty. Můžete definovat objekt, kde klíče reprezentují vaše pojmenované konstanty a hodnoty jsou literální řetězce nebo čísla.
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Error: Type '"GUEST"' is not assignable to type 'UserRole'.
function displayRole(role: UserRole) {
console.log(`User role is: ${role}`);
}
displayRole(userRoles.Admin); // Valid
displayRole("EDITOR"); // Valid
V tomto příkladu s objektem:
as const: Tato assertion činí celý objekt jen pro čtení. Důležitější je, že odvozuje literální typy pro všechny hodnoty vlastností (např."ADMIN"místostring) a činí samotné vlastnosti jen pro čtení.keyof typeof userRoles: Tento výraz má za následek union klíčů objektuuserRoles, což je"Admin" | "Editor" | "Viewer".typeof userRoles[keyof typeof userRoles]: Toto je lookup type. Vezme union klíčů a použije jej k vyhledání odpovídajících hodnot v typuuserRoles. Výsledkem je union hodnot:"ADMIN" | "EDITOR" | "VIEWER", což je náš požadovaný typ pro role.
Výstup JavaScriptu pro userRoles bude prostý JavaScriptový objekt:
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
To je výrazně lehčí než typický enum.
Kdy Použít Const Assertions
- Konstanty jen pro čtení: Když potřebujete pevnou sadu řetězcových nebo číselných literálů, které by se neměly měnit za běhu.
- Minimální výstup JavaScriptu: Pokud máte obavy o velikost balíčku a chcete nejvýkonnější běhovou reprezentaci pro vaše konstanty.
- Struktura podobná objektu: Pokud upřednostňujete čitelnost párů klíč-hodnota, podobně jako byste strukturovali data nebo konfiguraci.
- Řetězcové sady: Zvláště užitečné pro reprezentaci stavů, typů nebo kategorií, které jsou nejlépe identifikovány popisnými řetězci.
Alternativa 2: Union Types
Union types vám umožňují deklarovat, že proměnná může obsahovat hodnotu jednoho z několika typů. V kombinaci s literálními typy (řetězec, číslo, booleovské literály) tvoří výkonný způsob, jak definovat sadu povolených hodnot, aniž byste potřebovali explicitní deklaraci konstanty pro samotnou sadu.
Union Types s Řetězcovými Literály
Můžete přímo definovat union řetězcových literálů.
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Error: Type '"BLUE"' is not assignable to type 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Changing light to: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Error
Toto je nejpřímější a často nejstručnější způsob, jak definovat sadu povolených řetězcových hodnot.
Union Types s Číselnými Literály
Podobně můžete použít číselné literály.
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Error: Type '201' is not assignable to type 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("Success!");
} else {
console.log(`Error code: ${code}`);
}
}
handleResponse(500);
Kdy Použít Union Types
- Jednoduché, přímé sady: Když je sada povolených hodnot malá, jasná a nevyžaduje popisné klíče kromě samotných hodnot.
- Implicitní konstanty: Když nepotřebujete odkazovat na pojmenovanou konstantu pro samotnou sadu, ale spíše přímo používáte literální hodnoty.
- Maximální stručnost: Pro přímočaré scénáře, kde se definování vyhrazeného objektu nebo pole zdá jako zbytečné.
- Parametry/návratové typy funkcí: Vynikající pro definování přesné sady přijatelných řetězcových nebo číselných vstupů/výstupů pro funkce.
Porovnání Enumů, Const Assertions a Union Types
Pojďme shrnout klíčové rozdíly a případy použití:
Běhové Chování
- Enumy: Generují JavaScriptové objekty, potenciálně s reverzním mapováním.
- Const Assertions (Pole/Objekty): Generují prosté JavaScriptové pole nebo objekty. Informace o typu jsou vymazány za běhu, ale datová struktura zůstává.
- Union Types (s literály): Žádná běhová reprezentace pro samotný union. Hodnoty jsou jen literály. Typová kontrola probíhá čistě v době kompilace.
Čitelnost a Expresivita
- Enumy: Vysoká čitelnost, zejména s popisnými názvy. Může být více rozvláčné.
- Const Assertions (Objekty): Dobrá čitelnost prostřednictvím párů klíč-hodnota, napodobující konfigurace nebo nastavení.
- Const Assertions (Pole): Méně čitelné pro reprezentaci pojmenovaných konstant, více pro pouhý uspořádaný seznam hodnot.
- Union Types: Velmi stručné. Čitelnost závisí na jasnosti samotných literálních hodnot.
Typová Bezpečnost
- Všechny tři přístupy nabízejí silnou typovou bezpečnost. Zajišťují, že proměnným lze přiřadit pouze platné, předdefinované hodnoty nebo je předat funkcím.
Velikost Balíčku
- Enumy: Obecně největší kvůli generovaným JavaScriptovým objektům.
- Const Assertions: Menší než enumy, protože produkují prosté datové struktury.
- Union Types: Nejmenší, protože negenerují žádnou specifickou běhovou datovou strukturu pro samotný typ, pouze se spoléhají na literální hodnoty.
Matice Případů Použití
Zde je rychlý průvodce:
| Funkce | TypeScript Enum | Const Assertion (Objekt) | Const Assertion (Pole) | Union Type (Literály) |
|---|---|---|---|---|
| Běhový Výstup | JS Objekt (s reverzním mapováním) | Prostý JS Objekt | Prosté JS Pole | Žádný (pouze literální hodnoty) |
| Čitelnost (Pojmenované Konstanty) | Vysoká | Vysoká | Střední | Nízká (hodnoty jsou názvy) |
| Velikost Balíčku | Největší | Střední | Střední | Nejmenší |
| Flexibilita | Dobrá | Dobrá | Dobrá | Vynikající (pro jednoduché sady) |
| Běžné Použití | Stavy, Stavové Kódy, Kategorie | Konfigurace, Definice Rolí, Příznaky Funkcí | Uspořádané seznamy neměnných hodnot | Parametry funkcí, jednoduché omezené hodnoty |
Praktické Příklady a Osvědčené Postupy
Příklad 1: Reprezentace Stavových Kódů API
Enum:
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Const Assertion (Objekt):
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Union Type:
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Doporučení: Pro tento scénář je union type často nejstručnější a nejefektivnější. Samotné literální hodnoty jsou dostatečně popisné. Pokud byste potřebovali ke každému stavu přiřadit další metadata (např. uživatelsky přívětivou zprávu), byl by lepší volbou objekt const assertion.
Příklad 2: Definování Uživatelských Rolí
Enum:
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... logic ...
}
Const Assertion (Objekt):
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Union Type:
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Doporučení: Objekt const assertion zde představuje dobrou rovnováhu. Poskytuje jasné páry klíč-hodnota (např. userRolesObject.Admin), které mohou zlepšit čitelnost při odkazování na role, a přitom jsou stále výkonné. Union type je také velmi silný konkurent, pokud jsou dostatečné přímé řetězcové literály.
Příklad 3: Reprezentace Možností Konfigurace
Představte si konfigurační objekt pro globální aplikaci, která může mít různé motivy.
Enum:
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Const Assertion (Objekt):
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Union Type:
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Doporučení: Pro nastavení konfigurace, jako jsou motivy, je objekt const assertion často ideální. Jasně definuje dostupné možnosti a jejich odpovídající řetězcové hodnoty. Klíče (Light, Dark, System) jsou popisné a mapují se přímo na hodnoty, takže je konfigurační kód velmi srozumitelný.
Výběr Správného Nástroje pro Danou Práci
Rozhodnutí mezi TypeScript enumy, const assertions a union types není vždy černobílé. Často jde o kompromis mezi běhovým výkonem, velikostí balíčku a čitelností/expresivitou kódu.
- Zvolte Union Types, když potřebujete jednoduchou, omezenou sadu řetězcových nebo číselných literálů a požadujete maximální stručnost. Jsou vynikající pro podpisy funkcí a základní omezení hodnot.
- Zvolte Const Assertions (s Objekty), když chcete strukturovanější a čitelnější způsob definování pojmenovaných konstant, podobný enumu, ale s výrazně menší běhovou režií. To je skvělé pro konfiguraci, role nebo jakoukoli sadu, kde klíče přidávají významný význam.
- Zvolte Const Assertions (s Poli), když jednoduše potřebujete neměnný uspořádaný seznam hodnot a přímý přístup přes index je důležitější než pojmenované klíče.
- Zvažte TypeScript Enumy, když potřebujete jejich specifické funkce, jako je reverzní mapování (i když je to v moderním vývoji méně časté), nebo pokud má váš tým silné preference a dopad na výkon je pro váš projekt zanedbatelný.
V mnoha moderních projektech TypeScript se setkáte s tím, že se přiklání k const assertions a union types oproti tradičním enumům, zejména pro řetězcové konstanty, kvůli jejich lepším výkonnostním charakteristikám a často jednoduššímu výstupu JavaScriptu.
Globální Aspekty
Při vývoji aplikací pro globální publikum jsou konzistentní a předvídatelné definice konstant klíčové. Volby, o kterých jsme diskutovali (enumy, const assertions, union types), všechny přispívají k této konzistenci tím, že vynucují typovou bezpečnost v různých prostředích a vývojářských lokalizacích.- Konzistence: Bez ohledu na zvolenou metodu je klíčová konzistence v rámci vašeho projektu. Pokud se rozhodnete používat objekty const assertion pro role, držte se tohoto vzoru v celé kódové základně.
- Internacionalizace (i18n): Při definování štítků nebo zpráv, které budou internacionalizovány, používejte tyto typově bezpečné struktury, abyste zajistili, že budou použity pouze platné klíče nebo identifikátory. Skutečné přeložené řetězce budou spravovány samostatně prostřednictvím knihoven i18n. Například pokud máte pole `status`, které může být "PENDING", "PROCESSING", "COMPLETED", vaše knihovna i18n by mapovala tyto interní identifikátory na lokalizovaný zobrazovaný text.
- Časová Pásma a Měny: I když to přímo nesouvisí s enumy, pamatujte, že při práci s hodnotami, jako jsou data, časy nebo měny, může typový systém TypeScriptu pomoci vynutit správné používání, ale pro přesné globální zpracování jsou obvykle nutné externí knihovny. Například union type `Currency` by mohl být definován jako `"USD" | "EUR" | "GBP"`, ale skutečná logika převodu vyžaduje specializované nástroje.
Závěr
TypeScript poskytuje bohatou sadu nástrojů pro správu konstant. Zatímco enumy nám dobře sloužily, const assertions a union types nabízejí přesvědčivé, často výkonnější alternativy. Pochopením jejich rozdílů a výběrem správného přístupu na základě vašich specifických potřeb – ať už jde o výkon, čitelnost nebo stručnost – můžete psát robustnější, udržitelnější a efektivnější kód TypeScript, který se globálně škáluje.
Přijetí těchto alternativ může vést k menším velikostem balíčků, rychlejším aplikacím a předvídatelnějšímu vývojářskému prostředí pro váš mezinárodní tým.